#!/usr/bin/env perl
use warnings;
use strict;
#
# Copyright (C) 2017-2019 Hamish Coleman
#
# Given an FL2 file from the Lenovo BIOS update, try to recognise which
# packing system was used and allow the EC firmware image inside it to
# be extracted or reinserted
#
# Note that this script is just intended to deal with the container and
# any actions needed to extract or insert into the container - It does
# not check any checksums within the IMG - that should be left to
# the img manipulating tools (like mec tools).
#
# This script now supports extracting a couple of FL1 files, so its name
# is starting to be wrong.

package log;
use warnings;
use strict;
#
# Some simple logging
#

my @log;

sub add {
    my @entry = caller();
    push @entry, @_;
    push @log, \@entry;
}

sub print {
    my $self = shift;
    for my $entry (@log) {
        my $package = shift @{$entry};
        my $filename = shift @{$entry};
        my $line = shift @{$entry};
        printf("%s:%i %s ", $filename, $line, $package);
        printf(@{$entry});
        print("\n");
    }
}

1;

package FL2::base;
use warnings;
use strict;
#
# The base class for all the various types of FL2 encapsulation
#

use IO::File;

sub new {
    my $class = shift;
    my $fh = shift || return undef; # must have a file handle
    my $self = {};
    bless $self, $class;
    $self->{fh} = $fh;
    $self->{filesize} = (stat($self->{fh}))[7];

    return $self->_check();
}

sub set_offset_size {
    my $self = shift;
    $self->{offset} = shift;
    $self->{size} = shift;
    return $self;
}

sub offset { return shift->{offset}; }
sub size { return shift->{size}; }


sub get_block {
    my $self = shift;
    my $offset = shift;
    my $size = shift;

    return undef if (!$self->{fh}->seek($offset, 0));

    my $buf;
    my $count = $self->{fh}->sysread($buf, $size);

    return undef if ($count != $size);

    return \$buf;
}

# The base class simply says "no" to any checks - override to use
sub _check {
    return undef;
}

# Once a subclass has set the length and size, we can go and double check it
# has the expected copyright message
sub _check_copyright {
    my $self = shift;

    my $expect = "(C) Copyright IBM Corp. 2001, 2005 All Rights Reserved ";
    my $check_size = length($expect);
    my @offsets = (
        0x268,  # ARCompact 32bit platforms
        0x264,  # older 16bit platforms
        0x1480, # _EC file with second header (p15)
    );

    while (@offsets) {
        my $check_offset = shift @offsets;

        my $buf = $self->get_block($self->offset()+$check_offset, $check_size);

        return undef if (!defined($buf));

        if ($$buf eq $expect) {
            $self->{flag}{copyright}=$check_offset;
            return $self;
        }
    }

    log::add("no copyright (offset=0x%x)",$self->offset());
    return undef;
}

# Helper function, called by the real classes if they support extraction
sub _extract {
    my $self = shift;
    my $imgfile = shift;

    my $buf = $self->get_block($self->offset(),$self->size());
    die("bad read") if (!defined($buf));

    my $fh = IO::File->new($imgfile, "w");
    if (!defined($fh)) {
        warn("Could not open $imgfile: $!");
        return undef;
    }

    my $count = $fh->syswrite($$buf);
    if ($count != $self->size()) {
        unlink($imgfile);
        die("bad write");
    }

    return $self;
}

# Helper function, called by the real classes if they support insertion
sub _insert {
    my $self = shift;
    my $imgfile = shift;

    my $fh = IO::File->new($imgfile, "r");
    if (!defined($fh)) {
        warn("Could not open $imgfile: $!");
        return undef;
    }

    my $buf;
    my $count = $fh->sysread($buf, $self->size());
    if ($count != $self->size()) {
        unlink($imgfile);
        die("bad read");
    }

    if (!$self->{fh}->seek($self->offset(), 0)) {
        unlink($imgfile);
        die("bad seek");
    }

    $count = $self->{fh}->syswrite($buf);
    if ($count != $self->size()) {
        unlink($imgfile);
        die("bad write");
    }

    return $self;
}

1;

package FL2::prefix_ff;
use warnings;
use strict;
#
# Look for FL2 files that consist entirely of 0xff up until the IMG
#
# All the files that used to match this pattern now match one of the checkers
# with better signatures
#

use base qw(FL2::base);

sub _check {
    my $self = shift;

    # List of known file sizes (basically a doublecheck on the signature)
    my $known = {
         8523776 => [0x500000, 0x20000],    # x220 8DHT34WW
        12718080 => [0x500000, 0x30000],    # xx30
        16912384 => [0x500000, 0x30000],
    };

    my $check_offset = 0;
    my $check_size = 0x1000;
    my $buf = $self->get_block($check_offset, $check_size);

    if (!defined($buf)) {
        log::add("bad get_block");
        return undef;
    }

    my $try_data = $known->{$self->{filesize}};
    if (!defined($try_data)) {
        log::add("not in known sizes");
        return undef;
    }

    if ($$buf ne "\xff"x$check_size) {
        log::add("failed 0xff check");
        return undef;
    }

    $self->{flag}{encrypted}="yes";
    # All current examples of this format were encrypted

    # Loop through the known offset,size pairs for this filesize,
    # looking for one that has a matching copyright
    while (scalar(@{$try_data})) {
        my $try_offset = shift @{$try_data};
        my $try_size = shift @{$try_data};
        $self->set_offset_size($try_offset, $try_size);

        my $match = $self->_check_copyright();
        if ($match) {
            return $match;
        }
    }
    log::add("no copyright message");

    return undef;
}

sub extract {
    return shift->_extract(shift);
}

sub insert {
    return shift->_insert(shift);
}

1;

package FL2::prefix_garbage;
use warnings;
use strict;
#
# Look for FL2 files that have garbage at the beginning and large areas of
# 0xff before the IMG
#

use base qw(FL2::base);

sub _check {
    my $self = shift;

    # List of known file sizes (basically a doublecheck on the signature)
    # When found, Data is:
    #  [0] offset of EC firmware
    #  [1] size of EC firmware
    #  [2] offset of block to check for all 0xff as a signature check
    my $known = {
         4213270 => [0x290000, 0x20000, 0x21000],   #
         4240490 => [0x290000, 0x20000, 0x21000],   # at least two x200 images
    };

    # If the filesize is not in our known database, return no match
    my $known_data = $known->{$self->{filesize}};
    if (!defined($known_data)) {
        log::add("not in known sizes");
        return undef;
    }

    my $check_size = 0x1000;
    my $buf = $self->get_block($known_data->[2], $check_size);

    if (!defined($buf)) {
        log::add("bad get_block");
        return undef;
    }

    if ($$buf ne "\xff"x$check_size) {
        log::add("failed 0xff check");
        return undef;
    }

    $self->{flag}{encrypted}="no";
    # All current examples of this format were not encrypted

    $self->set_offset_size(@{$known_data});
    return $self->_check_copyright();
}

sub extract {
    return shift->_extract(shift);
}

sub insert {
    return shift->_insert(shift);
}

1;

package FL2::prefix_head_NAPI;
use warnings;
use strict;
#
# Look for FL2 files that have a "NAPI" header, followed by 0xff, followed
# by the IMG
#
# This was first seen in old x60 FL2 files

use base qw(FL2::base);

sub _find_napi {
    my $self = shift;
    my $offset = 0;

    while ($offset < $self->{filesize}) {
        my $buf = $self->get_block($offset, 4);
        return undef if (!defined($buf));
        $buf = $$buf;

        if ($buf eq 'NAPI') {
            return $offset;
        }

        $offset+=0x10000;
    }

    return undef; # not found
}


sub _check {
    my $self = shift;

    my $header_offset = $self->_find_napi();
    if (!defined($header_offset)) {
        log::add("no NAPI header found");
        return undef;
    }

    my $header_size = 0x20;
    my $buf = $self->get_block($header_offset, $header_size);

    if (!defined($buf)) {
        log::add("bad get_block");
        return undef;
    }

    my @fields = qw(
        signature1 unk1 unk2 signature2 unk3 unk4 unk5 signature3
        unk6 unk7 unk8 all_00
    );
    my @values = unpack("a4vCa4vvva4vVVc",$$buf);
    map { $self->{header}{$fields[$_]} = $values[$_] } (0..scalar(@fields)-1);

    if ($self->{header}{signature1} ne "NAPI") {
        log::add("failed signature");
        return undef;
    }
    if ($self->{header}{signature2} ne "ESCD") {
        log::add("failed signature");
        return undef;
    }
    if ($self->{header}{signature3} ne "ACFG") {
        log::add("failed signature");
        return undef;
    }

    # because I have no idea about the actual format of this header, we check
    # every field
    if (($self->{header}{unk1} != 0x000f) ||
        ($self->{header}{unk2} != 0x8d) ||
        ($self->{header}{unk3} != 0x000f) ||
        ($self->{header}{unk4} != 0x000e) ||
        ($self->{header}{unk5} != 0x000e) ||
        ($self->{header}{unk6} != 0x0200) ||
        ($self->{header}{unk7} != 0x00000000) ||
        ($self->{header}{unk8} != 0x0000fedf) ||
        ($self->{header}{all_00} != 0x00)
        ) {
        die("Unexpected NAPI header data");
        # die instead of simply logging, since we have matched a signature
        # and dont want to let this fall through to another checker
    }

    # TODO
    # - there is one more field at offset 0x1fff, containing a 0x02

    my $check_offset = 0x2000;
    my $check_size = 0x1000;
    $buf = $self->get_block($check_offset, $check_size);

    if (!defined($buf)) {
        log::add("bad get_block");
        return undef;
    }

    if ($$buf ne "\xff"x$check_size) {
        log::add("failed 0xff check");
        return undef;
    }

    $self->{flag}{encrypted}="no";
    # All current examples of this format were not encrypted

    $self->set_offset_size($header_offset + 0x10000, 0x20000);
    return $self->_check_copyright();
}

sub extract {
    return shift->_extract(shift);
}

sub insert {
    return shift->_insert(shift);
}

1;

package FL2::prefix_nothing;
use warnings;
use strict;
#
# Some FL2 files are simply the IMG data, with no header or container.
# For these, we can check the copyright string from inside the IMG
#

use base qw(FL2::base);

sub _check {
    my $self = shift;

    # List of known file sizes (basically a doublecheck on the signature)
    my $known = {
         196608 => [0, 0x30000],    # x240 GIHT32WW
    };

    if (!defined($known->{$self->{filesize}})) {
        log::add("not in known sizes");
        return undef;
    }

    $self->{flag}{encrypted}="yes";
    # All current examples of this format were encrypted

    $self->set_offset_size(@{$known->{$self->{filesize}}});
    return $self->_check_copyright();
}

sub extract {
    return shift->_extract(shift);
}

sub insert {
    return shift->_insert(shift);
}

1;

package FL2::prefix_head_EC;
use warnings;
use strict;
#
# Look for FL2 files that have a "_EC" prefix header at their start
#

use base qw(FL2::base);

sub _check {
    my $self = shift;

    # List of known file sizes (basically a doublecheck on the signature)
    # We also store the encryption flag in here
    my $known = {
         196896 => 'yes',
         229728 => 'unk', # p15
         262176 => 'no',
         #262432          # p15g2
         286752 => 'no',
    };

    my $header_offset = 0;
    my $header_size = 0x20;
    my $trailer_size = 0x100;

    my $buf = $self->get_block($header_offset, $header_size+0x20);

    if (!defined($buf)) {
        log::add("bad get_block");
        return undef;
    }

    my @fields = qw(
        signature version filesize imgsize hash_algo sign_algo
        hash_crc16 header_crc16 unk1
        signature2 unk2 imgmax imgsize2
    );
    my @values = unpack("a3CVVVVSSa8a4a17VV",$$buf);
    map { $self->{header}{$fields[$_]} = $values[$_] } (0..scalar(@fields)-1);

    if ($self->{header}{signature} ne "_EC") {
        log::add("failed signature");
        return undef;
    }
    if ($self->{header}{version} != 1) {
        log::add("failed version");
        return undef;
    }
    if ($self->{header}{filesize} != $self->{filesize}) {
        log::add("failed header filesize");
        return undef;
    }

    if (!defined($known->{$self->{filesize}})) {
        log::add("not in known sizes");
        return undef;
    }

    my $imgsize = $self->{header}{imgsize};

    if ($self->{header}{signature2} eq "^M;*") {
        # a second header with a different imgsize value in it !?
        log::add("found signature2");
        $header_size = 0x60;
        $imgsize = $self->{header}{imgsize2};
    }

    # p15g2 has a second _EC header, with what looks like the same fields
    # as the first one (unlike the above ^M;* header) but it also has a
    # ^M;* header slightly later in the file ...

    if ($imgsize+$header_size+$trailer_size == $self->{filesize}) {
        # there is an additional block of 0x100 appended to the FL2, outside
        # of the header defined IMG file
        # - I expect this is the digital signature
        $self->{flag}{trailer}="external";
        log::add("trailer external");
    } elsif ($imgsize+$header_size == $self->{filesize}) {
        # the additional block still looks like it is there, but they have
        # changed the accounting to count it in the IMG - these files are also
        # significantly larger in size
        $self->{flag}{trailer}="internal";
        log::add("trailer internal");
    } else {
        log::add("unexpected filesize/imgsize results");
        return undef;
    }

    # FIXME
    # - is that a csum? how to generate it and check it?

    $self->{flag}{encrypted} = $known->{$self->{filesize}};

    $self->set_offset_size($header_size, $imgsize);
    return $self->_check_copyright();
}

sub extract {
    return shift->_extract(shift);
}

# Note, no insert() will work until we know how to generate the checksum
#sub insert {
#    my $self = shift;
#    my $imgfile = shift;
#
#    my $write = $self->_insert($imgfile);
#
#    Calculate checksum
#    write checksum to header
#}


1;

package FL1::PFH_header;
use warnings;
use strict;
#
# Look for FL1 files that have a "$PFH" prefix header near their end
#
# Seen in at least the l530 firmware updates
#
# Thanks for skochinsky for the details:
# https://github.com/hamishcoleman/thinkpad-ec/issues/46
#

use base qw(FL2::base);

sub _find_pfh {
    my $self = shift;
    my $offset = $self->{filesize}-4;

    while ($offset > 0) {
        my $buf = $self->get_block($offset, 4);
        return undef if (!defined($buf));
        $buf = $$buf;

        if ($buf eq '$PFH') {
            log::add("found PFH header signature at offset=0x%x", $offset);
            return $offset;
        }

        $offset-=0x04;
    }

    log::add("no PFH found");
    return undef; # not found
}

sub _check {
    my $self = shift;

    # List of known file sizes (basically a doublecheck on the signature)
    my $known = {
         4681808 => 1,  # l440
         9437264 => 1,  # l430
        13631568 => 1,  # e330
        12587008 => 1,
    };

    my $capsule_offset_hack = $self->{capsule_offset_hack} || 0;

    my $header_offset = $self->_find_pfh();
    return undef if (!defined($header_offset));

    my $header_size = 4+4+4+2+4+2+4+4;

    my $buf = $self->get_block($header_offset, $header_size);

    if (!defined($buf)) {
        log::add("bad get_block");
        return undef;
    }
    if (!defined($known->{$self->{filesize}})) {
        log::add("not in known sizes");
    }

    my @fields = qw(
        signature version headersize headerchecksum
        totalimagesize totalimagechecksum
        numberofimages imagetableoffset
    );
    my @values = unpack("a4VVvVvVV",$$buf);
    map { $self->{header}{$fields[$_]} = $values[$_] } (0..scalar(@fields)-1);

    if ($self->{header}{signature} ne '$PFH') {
        log::add("failed signature");
        return undef;
    }

    if ($self->{header}{version} ne 0x10002) {
        log::add("failed version");
        return undef;
    }

    # now load the partition table
    $buf = $self->get_block(
        $self->{header}{imagetableoffset}+$capsule_offset_hack,
        (4+4+8+4)*$self->{header}{numberofimages}
    );
    if (!defined($buf)) {
        log::add("bad get_block");
        return undef;
    }

    $buf = $$buf;

    while ($buf) {
        my $dir = {};
        @fields = qw(FileOffset Size FlashAddress NameOffset _rest);
        @values = unpack("VVQVa*",$buf);
        map { $dir->{$fields[$_]} = $values[$_] } (0..scalar(@fields)-1);
        $buf = $dir->{_rest};
        delete $dir->{_rest};

        my $buf2 = $self->get_block(
            $dir->{NameOffset}+$capsule_offset_hack,
            32 # TODO - just a guess at the max name size
        );
        if (!defined($buf2)) {
            log::add("bad get_block");
            return undef;
        }

        my $name = unpack("Z*",$$buf2);
        $dir->{Name} = $name;

        push @{$self->{header}{dir}}, $dir;

        if ($name eq 'Ec') {
            $self->set_offset_size(
                $dir->{FileOffset}+$capsule_offset_hack,
                $dir->{Size},
            );
            return $self;
        }
    }

    log::add("no subsection found with name 'Ec'");
    return undef;
}

sub extract {
    return shift->_extract(shift);
}

sub insert {
    # FIXME:
    # - there is a "totalimagechecksum" field
    #   (which we probably should be updating
    return shift->_insert(shift);
}

1;

package EFI::Capsule::PFH_header;
use warnings;
use strict;
#
# Adjust the usual PFH loader to cope with an offset
# (presumably caused by the Capsule wrappers)
#

use base qw(FL1::PFH_header);

sub _check {
    my $self = shift;

    $self->{capsule_offset_hack} = 0x1d0;
    # This is probably:
    # header1_size + header2_size + ?
    # 0x50         + 0x168        + 0x18
    #$self->{capsule_offset_hack}
    #    = $self->{header1}{HeaderSize} + $self->{header2}{HeaderLength};

    return $self->SUPER::_check();
}

1;

package EFI::Capsule::UNK_header;
use warnings;
use strict;
#
# Some FL1 files appear to have an unknown header after the EFI headers
#

use base qw(FL2::base);

sub _check {
    my $self = shift;

    my $header3_offset
        = $self->{header1}{HeaderSize} + $self->{header2}{HeaderLength};
    my $header3_length = 0x30;

    my $buf = $self->get_block($header3_offset, $header3_length);
    if (!defined($buf)) {
        log::add("bad get_block");
        return undef;
    }

    # d95fe99c196ee7409a8febad7f78e0c5
    my $maybe_uuid = "\xd9\x5f\xe9\x9c\x19\x6e\xe7\x40\x9a\x8f\xeb\xad\x7f\x78\xe0\xc5";

    my @fields = qw(
        all_ff
        unk01
        maybe_uuid
        maybe_offset
        unk02
    );
    my @values = unpack("a16a8a16VV",$$buf);
    map { $self->{header3}{$fields[$_]} = $values[$_] } (0..scalar(@fields)-1);

    if ($self->{header3}{maybe_uuid} ne $maybe_uuid) {
        log::add("failed header3 maybe_uuid");
        return undef;
    }

    # FIXME: This is just a guess
    my $ec_size = 0x20000;

    $self->set_offset_size(
        $header3_offset + $header3_length + $self->{header3}{maybe_offset},
        $ec_size
    );

    return $self;
}

sub extract {
    return shift->_extract(shift);
}

# no insert() will work until we know how to generate the checksums

1;

package EFI::Capsule;
use warnings;
use strict;
#
# Look for FL1 files that have a known EFI capsule prefix
#

use base qw(FL2::base);

sub _check {
    my $self = shift;

    # bd86663b760d3040b70eb5519e2fc5a0
    my $capsule_uuid = "\xbd\x86\x66\x3b\x76\x0d\x30\x40\xb7\x0e\xb5\x51\x9e\x2f\xc5\xa0";

    my $buf = $self->get_block(0,16);
    if (!defined($buf)) {
        log::add("bad get_block");
        return undef;
    }
    $buf = $$buf;

    if ($buf ne $capsule_uuid) {
        log::add("no capsule_uuid");
        return undef;
    }

    $buf = $self->get_block(0x10, 4);
    if (!defined($buf)) {
        log::add("bad get_block");
        return undef;
    }
    my $header1_size = unpack("V", $$buf);

    $buf = $self->get_block(0, $header1_size);

    if (!defined($buf)) {
        log::add("bad get_block");
        return undef;
    }

    my @fields = qw(
        UUID
        HeaderSize Flags CapsuleImageSize SequenceNumber InstanceId
    );
    my @values = unpack("a16VVVVa16",$$buf);
    map { $self->{header1}{$fields[$_]} = $values[$_] } (0..scalar(@fields)-1);

    if ($self->{header1}{CapsuleImageSize} != $self->{filesize}) {
        log::add("failed header1 filesize");
        return undef;
    }

    # d954937a68044a4481ce0bf617d890df
    my $firmware_uuid = "\xd9\x54\x93\x7a\x68\x04\x4a\x44\x81\xce\x0b\xf6\x17\xd8\x90\xdf";

    $buf = $self->get_block($header1_size, 0x38);

    if (!defined($buf)) {
        log::add("bad get_block");
        return undef;
    }

    @fields = qw(
        ZeroVector FileSystemGuid FvLength Signature
        Attributes HeaderLength Checksum
        Reserved
        Revision
    );
    @values = unpack("a16a16Q<a4Vvva3c",$$buf);
    map { $self->{header2}{$fields[$_]} = $values[$_] } (0..scalar(@fields)-1);

    #if ($self->{header2}{ZeroVector} ne '\x00'x16) {
    #    log::add("failed header2 zerovector");
    #    return undef;
    #}
    if ($self->{header2}{FileSystemGuid} ne $firmware_uuid) {
        log::add("failed header2 FileSystemGuid");
        return undef;
    }
    if ($self->{header2}{FvLength} != ($self->{filesize}-$header1_size)) {
        log::add("failed header1 filesize");
        return undef;
    }
    if ($self->{header2}{Signature} ne '_FVH') {
        log::add("failed header2 Signature");
        return undef;
    }

    # TODO:
    # - check Checksum
    #  "A 16-bit checksum of the firmware volume header. A valid header sums to zero"

    if ($self->{header2}{Revision} == 1) {
        # So far, the only revision 1 headers seen have also had a PFH header
        # (with an annoying offset)
        bless $self, 'EFI::Capsule::PFH_header';
        return $self->_check();
    }

    if ($self->{header2}{Revision} == 2) {
        # l440 has this

        bless $self, 'EFI::Capsule::UNK_header';
        return $self->_check();
    }

    return undef;
}


sub extract {
    return shift->_extract(shift);
}

# no insert() will work until we know how to generate the checksums

1;

# TODO
# - 8muj19us.iso has yet another format:
# It is all 0xff until 0x500000, however it then looks like it has a header.
# There is certainly not the normal copyright string, and the EC version
# string is in entirely the wrong place
# 00500000  45 43 20 49 4d 41 47 45  30 00 00 00 00 00 00 00  |EC IMAGE0.......|
# 00500010  d0 08 38 4d 48 54 37 39  57 57 00 00 00 20 02 00  |..8MHT79WW... ..|
# 00500020  00 00 00 00 31 36 01 00  00 00 00 00 0d b3 9e 00  |....16..........|

package main;
use warnings;
use strict;

use Data::Dumper;
$Data::Dumper::Indent = 1;
$Data::Dumper::Sortkeys = 1;
$Data::Dumper::Quotekeys = 0;

use IO::File;

# Call each of the detectors in turn to try to find the location of the IMG
sub detect_img {
    my $fl2name = shift;

    my $fh = IO::File->new($fl2name, "r+");
    if (!defined($fh)) {
        warn("Could not open $fl2name: $!");
        return undef;
    }

    my $object;

    # These checks have a good signature detection
    if (!defined($object)) {
        $object = FL2::prefix_head_EC->new($fh);
    }
    if (!defined($object)) {
        $object = EFI::Capsule->new($fh);
    }

    # These checks always do a file search
    if (!defined($object)) {
        $object = FL1::PFH_header->new($fh);
    }
    if (!defined($object)) {
        $object = FL2::prefix_head_NAPI->new($fh);
    }

    # these checks have very little in the way of signatures, so only
    # check for them after everything else
    if (!defined($object)) {
        $object = FL2::prefix_ff->new($fh);
    }
    if (!defined($object)) {
        $object = FL2::prefix_garbage->new($fh);
    }
    if (!defined($object)) {
        $object = FL2::prefix_nothing->new($fh);
    }

    return $object;
}

sub main() {
    # get args
    my $cmd = shift @ARGV;
    if (!defined($cmd)) {
        die("Need command");
    }

    my $fl2file = shift @ARGV;
    if (!defined($fl2file)) {
        die("Need FL2 filename");
    }

    my $imgfile = shift @ARGV;
    if ($cmd ne 'check' && !defined($imgfile)) {
        die("Need IMG filename");
    }

    # validity check args
    if ($cmd ne "from_fl2" && $cmd ne "to_fl2" && $cmd ne "check") {
        die("direction must be one of 'from_fl2' or 'to_fl2'");
    }

    my $verbose = shift @ARGV;
    if (defined($verbose) && $verbose eq 'verbose') {
        $verbose = 1;
    } else {
        $verbose = 0;
    }

    if ( ! -e $fl2file ) {
        die("FL2 file $fl2file must exist");
    }

    my $object = detect_img($fl2file);
    if (!defined($object)) {
        printf("Could not determine IMG details for %s\n", $fl2file);
        if ($verbose) {
            log::print();
        }
        exit(1);
    }
    printf("IMG at offset 0x%x size 0x%x (%s %s)\n",
        $object->offset(),
        $object->size(),
        ref($object),
        $fl2file,
    );

    if ($verbose) {
        print(Dumper($object));
        log::print();
    }

    if ($cmd eq 'from_fl2') {
        if (!$object->can('extract')) {
            die("FL2 container type cannot extract");
        }
        return $object->extract($imgfile);
    }

    if ($cmd eq 'to_fl2') {
        if (!$object->can('insert')) {
            die("FL2 container type cannot insert");
        }
        return $object->insert($imgfile);
    }
}
unless(caller) {
    main();
}
